package cn.sciento.transfer.infra.migration;

import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.ByteArrayOutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.DriverManager;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import liquibase.CatalogAndSchema;
import liquibase.Contexts;
import liquibase.Liquibase;
import liquibase.changelog.ChangeLogParameters;
import liquibase.changelog.DatabaseChangeLog;
import liquibase.database.Database;
import liquibase.database.DatabaseConnection;
import liquibase.database.DatabaseFactory;
import liquibase.database.jvm.JdbcConnection;
import liquibase.diff.DiffGeneratorFactory;
import liquibase.diff.DiffResult;
import liquibase.diff.compare.CompareControl;
import liquibase.diff.output.DiffOutputControl;
import liquibase.diff.output.ObjectChangeFilter;
import liquibase.diff.output.StandardObjectChangeFilter;
import liquibase.diff.output.changelog.DiffToChangeLog;
import liquibase.exception.ChangeLogParseException;
import liquibase.parser.ChangeLogParserFactory;
import liquibase.resource.ClassLoaderResourceAccessor;
import liquibase.resource.ResourceAccessor;
import liquibase.serializer.ChangeLogSerializerFactory;
import liquibase.snapshot.SnapshotControl;
import cn.sciento.transfer.infra.liquibase.parser.data.DatabaseChangeLogFileData;
import cn.sciento.transfer.infra.liquibase.parser.data.DatabaseChangeLogFileExt;
import cn.sciento.transfer.infra.liquibase.parser.data.STongStandardDiffGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
import org.yaml.snakeyaml.Yaml;

public class DBMigrationUtils {
  public static final Logger LOGGER = LoggerFactory.getLogger(DBMigrationUtils.class);
  
  public static final String INTERNAL_USER = "-1";
  
  static {
    DiffGeneratorFactory.getInstance().register(new STongStandardDiffGenerator());
  }
  
  public static Connection buildConnection(String url, String username, String password) throws Exception {
    Class.forName("com.mysql.jdbc.Driver");
    Connection connection = DriverManager.getConnection(url, username, password);
    return connection;
  }
  
  public static Database buildDatabase(Connection connection) throws Exception {
    JdbcConnection jdbcConnection = new JdbcConnection(connection);
    Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation((DatabaseConnection)jdbcConnection);
    return database;
  }
  
  public static DatabaseChangeLogFileData compareDatabase(Database sourceDatabase, Database targetDatabase, String sourceTableName, String targetTableName) throws Exception {
    LOGGER.debug("source table name: {}, target table name: {}.", sourceTableName, targetTableName);
    STongStandardDiffGenerator.setDiffTableName(sourceTableName, targetTableName);
    DiffResult diffResult = DiffGeneratorFactory.getInstance().compare(sourceDatabase, targetDatabase, new SnapshotControl(sourceDatabase, (ObjectChangeFilter)new StandardObjectChangeFilter(StandardObjectChangeFilter.FilterType.INCLUDE, "table:" + sourceTableName), new Class[0]), new SnapshotControl(targetDatabase, (ObjectChangeFilter)new StandardObjectChangeFilter(StandardObjectChangeFilter.FilterType.INCLUDE, "table:" + targetTableName), new Class[0]), new CompareControl());
    STongStandardDiffGenerator.clearDiffTableName();
    CatalogAndSchema sourceCatalogAndSchema = sourceDatabase.getDefaultSchema();
    LOGGER.debug("================source schema: {}, catalog: {}====================", sourceCatalogAndSchema.getSchemaName(), sourceCatalogAndSchema.getCatalogName());
    CatalogAndSchema targetCatalogAndSchema = targetDatabase.getDefaultSchema();
    LOGGER.debug("================target schema: {}, catalog: {}====================", targetCatalogAndSchema.getSchemaName(), targetCatalogAndSchema.getCatalogName());
    DiffOutputControl diffOutputControl = new DiffOutputControl(false, false, true, new CompareControl.SchemaComparison[] { new CompareControl.SchemaComparison(sourceCatalogAndSchema, targetCatalogAndSchema) });
    DiffToChangeLog diffToChangeLog = new DiffToChangeLog(diffResult, diffOutputControl);
    String fileName = UUID.randomUUID().toString();
    String fileContent = buildDatabaseChangeLogContent(diffToChangeLog, "-1", DatabaseChangeLogFileExt.YAML);
    DatabaseChangeLogFileData databaseChangeLogFileData = new DatabaseChangeLogFileData(fileName, fileContent, DatabaseChangeLogFileExt.YAML_INMEMORY);
    return databaseChangeLogFileData;
  }
  
  public static String buildDatabaseChangeLogContent(DiffToChangeLog diffToChangeLog, String author, DatabaseChangeLogFileExt databaseChangeLogFileExt) throws Exception {
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    PrintStream printStream = new PrintStream(out, true, StandardCharsets.UTF_8.name());
    diffToChangeLog.setChangeSetAuthor(author);
    diffToChangeLog.print(printStream, ChangeLogSerializerFactory.getInstance().getSerializer(databaseChangeLogFileExt.ext()));
    printStream.close();
    return out.toString(StandardCharsets.UTF_8.name());
  }
  
  public static String updateDatabase(DatabaseChangeLogFileData databaseChangeLogFileData, Database targetDatabase) throws Exception {
    LOGGER.debug("enter update database...");
    DatabaseChangeLog databaseChangeLog = ChangeLogParserFactory.getInstance().getParser(databaseChangeLogFileData.getFileParserExt().ext(), (ResourceAccessor)new ClassLoaderResourceAccessor()).parse((new ObjectMapper()).writeValueAsString(databaseChangeLogFileData), new ChangeLogParameters(), (ResourceAccessor)new ClassLoaderResourceAccessor());
    Liquibase liquibase = new Liquibase(databaseChangeLog, (ResourceAccessor)new ClassLoaderResourceAccessor(), targetDatabase);
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    OutputStreamWriter outputStreamWriter = new OutputStreamWriter(out, StandardCharsets.UTF_8.name());
    liquibase.update(new Contexts(), outputStreamWriter);
    outputStreamWriter.close();
    String updateDatabaseResult = out.toString(StandardCharsets.UTF_8.name());
    LOGGER.debug("simulate update database with result: [{}].", updateDatabaseResult);
    LOGGER.debug("start actual update database...");
    liquibase.update(new Contexts());
    LOGGER.debug("end actual update database...");
    return updateDatabaseResult;
  }
  
  public static void stripRemarkChanges(DatabaseChangeLogFileData databaseChangeLogFileData) throws Exception {
    Yaml yaml = new Yaml();
    Map parsedYaml = (Map)yaml.load(databaseChangeLogFileData.getFileContent());
    Object rootList = parsedYaml.get("databaseChangeLog");
    if (rootList == null) {
      return;
    }
    if (!(rootList instanceof List)) {
      throw new ChangeLogParseException("databaseChangeLog does not contain a list of entries. Each changeSet must begin ' - changeSet:'");
    }
    List<Map> removedRootItemList = new ArrayList<>();
    for (Object rootItem : (List)rootList) {
      if (rootItem instanceof Map && ((Map)rootItem).containsKey("changeSet")) {
        Object changeSet = ((Map)rootItem).get("changeSet");
        if (changeSet instanceof Map && ((Map)changeSet).containsKey("changes")) {
          Object changes = ((Map)changeSet).get("changes");
          if (changes instanceof List) {
            List<Map> removedItemList = new ArrayList<>();
            ((List)changes).forEach(item -> {
                  if (item instanceof Map && ((Map)item).containsKey("setColumnRemarks")) {
                    ((Map)item).remove("setColumnRemarks");
                  }
                  if (CollectionUtils.isEmpty((Map)item)) {
                    removedItemList.add((Map)item);
                  }
                });
            ((List)changes).removeAll(removedItemList);
            if (CollectionUtils.isEmpty((List)changes)) {
              removedRootItemList.add((Map)rootItem);
            }
          } 
        } 
      } 
    } 
    ((List)rootList).removeAll(removedRootItemList);
    if (CollectionUtils.isEmpty((List)rootList)) {
      databaseChangeLogFileData.setFileContent(null);
    } else {
      databaseChangeLogFileData.setFileContent(yaml.dump(parsedYaml));
    } 
  }
}
